##Pacote de analise
##Reconhecimento de conteudo harmonico em arquivos de audio

import sys
import wave
import audioop
import numpy as np
import math
import itertools
from stft import stft
#Importacao de janelas para serem usadas na STFT
from numpy import hanning,hamming, blackman
from Constantes import qt,vetornotas,mt,C0,notas_acordes,harmonicos_medios,max_hz,vetoracordes
from Utils import inicia_dict, buscaFreq,derivadadif,maior_item

#Um arquivo sonoro
class Audio():
    #Parametros: nome, o nome do arquivo de audio
    def __init__(self,nome):
        self.__arquivo = wave.open(nome,"r")
        self.__samples = self.__frames__()
    #Traduz os frames do arquivo de audio para valores numericos
    #Retorna:uma lista contendo os samples em forma numerica
    def __frames__(self):
        n = self.__arquivo.getnframes()
        wid = self.__arquivo.getsampwidth()
        conteudo = self.__arquivo.readframes(n)
        samples = [audioop.getsample(conteudo, wid, i) for i in range(n)]
        return samples
    #Retorna o framerate do arquivo
    def framerate(self):
        return self.__arquivo.getframerate()
    #Retorna o espectro da fft do arquivo total
    def espectro(self):
        self.spectrum = np.fft.fft(self.__samples)
        return np.abs(self.spectrum)
    #Retorna uma lista das frequencias do espectro fft do arquivo. 
    def freq_espectro(self):
        return np.fft.fftfreq(len(self.spectrum))
    #gera um espectrograma do arquivo de audio.
    #Parametros: win, uma janela para calculo da Short-Time-Fourier-Transform, hamming por padrao
    #           fft_len, o passo para o calculo de STFT (pode ser necessario para nao sobrecarregar a memoria)
    def gera_espectrograma(self,win=hamming(32),overlap=None):
        spec = np.abs(stft(self.__samples,win,overlap))
        return spec

#Um espectro de amplitude de um sinal
class Espectro:
    #Parametros: spec, um espectro de amplitude
    def __init__(self,spec,framerate):
        frequencias = np.fft.fftfreq(len(spec))
        if len(frequencias)%2==0 :
            self._frequencias = frequencias[:len(frequencias)/2]
            self._espectro = spec[:len(spec)/2]
        else:
            self._frequencias = frequencias[:len(frequencias)/2+1]
            self._espectro = spec[:len(spec)/2+1]
        self.framerate=framerate
    def __len__(self):
        return len(self._espectro)
    #Apresenta os pontos maximos do espectro
    #Parametros:    threshold, um valor de limite para consideracao dos pontos maximos
    def pontos_maximos(self,threshold=10):
        dev = derivadadif(self._frequencias,self._espectro)
        maximo = max(dev)
        pontosextremos = []
        for i in range(len(dev)-1):
            if (dev[i]>=maximo/threshold and dev[i]>=0 and dev[i+1]<0):
                pontosextremos.append(i)
        return pontosextremos
    #Retorna: as frequencias do espectro
    def frequencias(self):
        return self._frequencias
    #Retorna: O espectro
    def espectro(self):
        return self._espectro
    #Retorna a frequencia maxima do espectro
    #Parametros:    framerate, o framerate do sinal
    #Retorna: o maior valor de frequencia do espectro
    def freqmax(self):
	    frequencias = self._frequencias
	    indicemax = np.argmax(self._espectro)
	    freqmaxima = frequencias[indicemax]
	    freqmaxima = abs(freqmaxima*self.framerate)
	    return freqmaxima
    def constroi_MessengerPicos(self):
        messenger = MessengerPicos(self.frequencias(),self.espectro(),self.pontos_maximos(),self.framerate)
        return messenger

#Um espectograma, uma representacao frequencia-tempo do audio
class Espectrograma:
    #Parametros: spec, o espectrograma do sinal
    def __init__(self,spec,framerate):
        frequencias = np.fft.fftfreq(spec.shape[1])
        if len(frequencias)%2==0:
            self._frequencias = frequencias[:len(frequencias)/2]
            n,m=spec.shape[0],spec.shape[1]/2
            spec.resize((n,m),refcheck=False)
        else:
            self._frequencias = frequencias[:len(frequencias)/2+1]
            n,m=spec.shape[0],spec.shape[1]/2+1
            spec.resize((n,m),refcheck=False)
        self._espectros=spec
        self.framerate=framerate
    def __len__(self):
        return self._espectros.shape[0]
    #Retorna os pontos maximos do espectro[i] do espectrograma
    #Parametros:    threshold, um valor de limite para consideracao dos pontos maximos
    #               i, o indice do espectro
    def pontos_maximos(self,i,threshold=10):
        if i<len(self):
            espectro = self._espectros[i]
            frequencias = self.frequencias()
            dev = derivadadif(frequencias,espectro)
            maximo = max(dev)
            pontosextremos = []
            for i in range(len(dev)-1):
                if (dev[i]>=maximo/threshold and dev[i]>=0 and dev[i+1]<0):
                    pontosextremos.append(i)
            return pontosextremos
    #Retorna: as frequencias do espectro
    def frequencias(self):
        return self._frequencias
    #Retorna um espectro[i] do espectograma
    #Parametros:    n, o indice do espectro
    #Retorna: as frequencias do espectro
    def espectro(self,n):
        if n<self._espectros.shape[0]:
            return self._espectros[n]
    #Retorna a frequencia maxima do espectro[i]
    #Parametros: i, o indice do espectro
    #           framerate, o framerate do sinal
    def freqmax(self,i):
	    frequencias = self.frequencias()
	    indicemax = np.argmax(self.espectro(i))
	    freqmaxima = frequencias[indicemax]
	    freqmaxima = abs(freqmaxima*self.framerate)
	    return freqmaxima
    def constroi_MessengerPicos(self,i):
        messenger = MessengerPicos(self.frequencias(),self.espectro(i),self.pontos_maximos(i),self.framerate)
        return messenger

#Messenger, para comunicacao com Picos
class MessengerPicos:
    def __init__(self,frequencias,espectro,pontos_max,framerate):
        self.frequencias=frequencias
        self.espectro=espectro
        self.pontos_max=pontos_max
        self.framerate=framerate

#Uma sequencia de pares ordenados representando picos de um espectro
class Picos:
    #Parametros: messenger, da classe MessengerPicos, feita para comunicacao com Picos
    def __init__(self,messenger):
        self.pares = [[ messenger.frequencias[j]*messenger.framerate, messenger.espectro[j] ] for j in messenger.pontos_max]
    #Gera um histograma de notas para as frequencias do espectro analisado
    #Parametros:    nharmonicos, o numero de harmonicos a serem considerados
    #               erro, o erro permitido na analise (padrao, um quarto de tom)
    #               espectro_ref , o espectro de referencia, por padrao
    #               constitue-se dos harmonicos_medios
    def gera_histograma(self,nharmonicos=6,erro=qt,espectro_ref=harmonicos_medios):
        pesos = inicia_dict()
        self.pares.sort(None,key = lambda dupla:dupla[1])
        frequencias = [p[0] for p in self.pares]
        if nharmonicos > len(espectro_ref)-1:
            nharmonicos = len(espectro_ref)-1
        for j in frequencias:
            if (j <= max_hz):
                p = 0
                for i in range(2,nharmonicos):
                    busca = buscaFreq(frequencias,i*j,erro)
                    if (busca != -1):
                        p=p+busca*math.pow(10,espectro_ref[i]/10)
                N = Nota(j)
                pesos[N]=pesos[N]+p
        return pesos

#Um histograma que apresenta a distribuicao de notas de um sinal de audio
class Histograma:
    #Pamatros: dicionario, um dicionario de notas com os valores do histograma
    def __init__(self,dicionario):
        self.histograma=dicionario
    #Retorna uma triade de notas
    #Parametros:    n, notas maximas a serem levadas em conta no histograma para a geracao da triade
    #Retorna:   um conjunto (set) de tres notas
    def triade(self,n=5):
        maximoL = []
        maximo = Nota(None)
        for k in range(n):
            m = 0
            for i in self.histograma:
                if not (i in maximoL) and (self.histograma[i] > m ):
                    m = self.histograma[i]
                    maximo = i
            maximoL.append(maximo.valor)
        permut =[i for i in itertools.combinations(maximoL,3)]
        cont = 0
        achou = False
        while(achou!=True and cont<len(permut)):
            conjnotas = permut[cont]
            #tenta encontrar algum valor valido
            if(frozenset(conjnotas) in notas_acordes):
                achou = True
            cont = cont+1
        return conjnotas

#Uma classe de notas musicais
class Nota:
    #Parametros: valor, pode ser uma str contendo o nome da nota (como "A" ou "A#") ou uma frequencia em Hz
    def __init__(self,valor):
        if type(valor)==str or valor is None:
            if valor in vetornotas :
                self.valor = valor
        elif self.__num_valido(valor):
            self.valor = self.freqnota(valor)
        else:
            raise ValueError("Apenas str ou numeros. Recebido %s"%type(valor))
    #verifica se um tipo eh valido como numero para calculo de frequencia
    def __num_valido(self,n):
	valido=True
	try:
	    n=n+1
	except:
	    valido=False
	return valido
    #converte uma frequencia em Hz para uma nota
    def freqnota(self,hz):
        nota = None
        indice = 0
        frequencia = abs(hz)
        freqaux = C0
        achou = False
        while ( achou==False and freqaux < max_hz):
            if ( frequencia > freqaux/qt and frequencia<freqaux*qt):
                nota = vetornotas[indice]
                achou=True
            indice = (indice+1)%12
            freqaux=freqaux*mt
        return nota
    def __eq__(self,note):
        return self.valor ==  note
    def __hash__(self):
        return vetornotas.index(self.valor)
    def __str__(self):
        return str(self.valor)
    def __repr__(self):
        return self.__str__() 

#Uma classe para um acorde
class Acorde:
    #Parametros: conjnotas, um conjunto (set) de notas do acorde
    def __init__(self,conjnotas):
        if(frozenset(conjnotas) in notas_acordes):
            self.nome=notas_acordes[frozenset(conjnotas)]
            self.notas=conjnotas
        else:
            self.nome='nao'
            self.notas=None
    def __str__(self):
        return self.nome
    def __repr__(self):
        return self.__str__()
    def __eq__(self,acorde):
        return self.nome==self.nome

    #TODO
class Regiao:
    def funcao(s):
        a=Acorde()
        return a

    #TODO
class NucleoTom:
    __nucleo=[]
    def sub_regiao(i):
        a=Regiao()
        return a

#Classe responsavel pela analise de arquivos de audio
class Analise:
    #Parametros:    audio, um objeto do tipo Audio
    def __init__(self,audio):
        self.audio=audio
        self.frate = self.audio.framerate()
        self.espectro=None
        self.espectrograma=None
    #Analisa o arquivo de audio e retorna uma nota e o espectograma calculado
    #Parametros: window , uma janela para calculo da STFT
    #           overlap, o overlap do STFT
    def analisa_nota(self,window=np.ones(256),overlap=100):
        if self.espectrograma==None:
            lista_espectro=self.audio.gera_espectrograma(win=window,overlap=overlap)
            self.espectrograma = Espectrograma(lista_espectro,self.frate)
        i=0
        repet = inicia_dict(tipo="notas")
        while i<len(self.espectrograma):
            nota = Nota(self.espectrograma.freqmax(i))
            if nota.valor != None:
                repet[nota]+=1
            i+=1
        return maior_item(repet),self.espectrograma
    #Analisa o arquivo de audio e retorna um objeto do tipo acorde de o espectograma calculado
    def analisa_acorde(self):
        if self.espectro==None:
            self.espectro = self.audio.espectro()
        spec = Espectro(self.espectro,self.frate)
        messenger = spec.constroi_MessengerPicos()
        peak = Picos(messenger)
        histo = Histograma(peak.gera_histograma())
        return Acorde(histo.triade()),histo
    #Analisa o arquivo de audio e retorna um nome de tom
    #Parametros: window , uma janela para calculo da STFT
    #           overlap, o overlap do STFT
    def analisa_tonalidade(self,window=np.ones(512),overlap=100):
        if self.espectrograma==None:
            lista_espectro=self.audio.gera_espectrograma(win=window,overlap=overlap)
            self.espectrograma = Espectrograma(lista_espectro,self.frate)
        i=0
        notasacorde=[]
        freq = self.espectrograma.frequencias()
        repet = inicia_dict(tipo="acordes")
        while i<len(self.espectrograma):
            messenger = self.espectrograma.constroi_MessengerPicos(i)
            peak = Picos(messenger)
            histo = Histograma(peak.gera_histograma())
            nome_acorde = str(Acorde(histo.triade()))
            repet[nome_acorde]+=1
            i+=1
        return maior_item(repet),self.espectrograma
    #def analisa_fluxo():
    #Analisa o arquivo de audio e retorna um espectro
    def analisa_espectro(self):
        if self.espectro==None:
            self.espectro = self.audio.espectro()
        return self.espectro
